package snapex.expense.service.impl;

import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import static org.mybatis.dynamic.sql.SqlBuilder.isGreaterThanOrEqualTo;
import static org.mybatis.dynamic.sql.SqlBuilder.isNotNull;

import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import java.util.List;

import org.apache.commons.lang3.RandomStringUtils;
import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter;
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import snapex.core.Billable;
import snapex.core.ExpenseStatus;
import snapex.core.Operation;
import snapex.core.OperationNotAllowException;
import snapex.core.RecordNotFoundException;
import snapex.core.Site;
import snapex.core.ValidationFailureException;
import snapex.core.mapper.ExpenseCashAdvanceDynamicSqlSupport;
import snapex.core.mapper.ExpenseDetailDynamicSqlSupport;
import snapex.core.mapper.ExpenseDynamicSqlSupport;
import snapex.core.mapper.ext.ExpenseCashAdvanceMapperExt;
import snapex.core.mapper.ext.ExpenseDetailMapperExt;
import snapex.core.mapper.ext.ExpenseMapperExt;
import snapex.core.model.Expense;
import snapex.core.model.ExpenseCashAdvance;
import snapex.core.model.ExpenseDetail;
import snapex.core.model.Result;
import snapex.core.model.SearchParams;
import snapex.expense.service.ICurrencyRateService;
import snapex.expense.service.IExpenseService;

@Slf4j
@Service
@Transactional
public class ExpenseService implements IExpenseService {

	@Autowired
	private ExpenseMapperExt expenseMapper;

	@Autowired
	private ExpenseDetailMapperExt expenseDetailMapper;

	@Autowired
	private ExpenseCashAdvanceMapperExt expenseCashAdvanceMapper;
	
	@Autowired
	ICurrencyRateService currencyRateService;
	
	@Override
	public Result<Expense> getExpense(String expenseId, boolean isBasic) {
		
		Expense expense = expenseMapper.selectByPrimaryKey(expenseId);
		
		if (expense != null && !isBasic) {
			expense.setPurposes(expenseDetailMapper.selectByExample()
					.where(ExpenseDetailDynamicSqlSupport.expenseId, isEqualTo(expenseId)).build().execute());

			expense.setDeductions(expenseCashAdvanceMapper.selectByExample()
					.where(ExpenseCashAdvanceDynamicSqlSupport.expenseId, isEqualTo(expenseId)).build().execute());
		}

		return Result.success().data(expense);
	}

	@Override
	public Result<List<Expense>> getExpenses(SearchParams params) {

		QueryExpressionDSL<MyBatis3SelectModelAdapter<List<Expense>>>.QueryExpressionWhereBuilder queryWhere = expenseMapper
				.selectByExample().where(ExpenseDynamicSqlSupport.expenseId, isNotNull());
		
		if (params.getExpenseId() != null) {
			queryWhere.and(ExpenseDynamicSqlSupport.expenseId, isEqualTo(params.getExpenseId()));
		}

		if (params.getUsername()!= null) {
			queryWhere.and(ExpenseDynamicSqlSupport.username, isEqualTo(params.getUsername()));
		}

		if (params.getStatus() != null) {
			queryWhere.and(ExpenseDynamicSqlSupport.status, isEqualTo(params.getStatus()));
		}

		if(params.getSubmittedDate() != null) {
			queryWhere.and(ExpenseDynamicSqlSupport.submittedDate, isGreaterThanOrEqualTo(params.getSubmittedDate().toString()));
		}
		
		if(params.getProjectId() != null) {
			queryWhere.and(ExpenseDynamicSqlSupport.clarityProjectId, isEqualTo(params.getProjectId()));
		}
				
		List<Expense> results = queryWhere.orderBy(ExpenseDynamicSqlSupport.submittedDate).build().execute();

		return Result.success().data(results);
	}

	@Override
	public Result<List<Expense>> exportExpenses(SearchParams params){
		
		List<Expense> expenses = this.getExpenses(params).getData();
		
		if(!params.LEVEL_BASIC.equalsIgnoreCase(params.getLevel())) {
			for(Expense expense : expenses) {			
				expense.setPurposes(expenseDetailMapper.selectByExample()
						.where(ExpenseDetailDynamicSqlSupport.expenseId, isEqualTo(expense.getExpenseId())).build().execute());
	
				expense.setDeductions(expenseCashAdvanceMapper.selectByExample()
						.where(ExpenseCashAdvanceDynamicSqlSupport.expenseId, isEqualTo(expense.getExpenseId())).build().execute());
			}
		}
		
		return Result.success().data(expenses);
	}
	
	@Override
	public Result createExpense(Expense expense) {
		boolean isNew = true;
		String expenseId = expense.getExpenseId();
		if (StringUtils.isEmpty(expenseId)) {			
			do {
				expenseId = generateExpenseId();
				expense.setExpenseId(expenseId);
			}
			while(expenseMapper.selectByPrimaryKey(expenseId) != null);
			
		}else {
					
			isNew = false;
			int count = expenseDetailMapper.deleteByExample().where(ExpenseDetailDynamicSqlSupport.expenseId, isEqualTo(expenseId)).build().execute();
						
			count = expenseCashAdvanceMapper.deleteByExample().where(ExpenseCashAdvanceDynamicSqlSupport.expenseId, isEqualTo(expenseId)).build().execute();
			
		}		
		
		BigDecimal totalAmount = new BigDecimal(0);		
		if(expense.getPurposes() != null) {
			for(ExpenseDetail purpose : expense.getPurposes()) {
				
				totalAmount = totalAmount.add(currencyRateService.exchange(purpose.getCurrency(), purpose.getAmount())); 
				
				purpose.setExpenseId(expenseId);
				purpose.setExpenseDetailId(generateExpenseDetailId());
				expenseDetailMapper.insert(purpose);
			}			
		}
		
		expense.setTotalAmount(totalAmount);	
		
		BigDecimal totalCashAdvance = new BigDecimal(0);
		if(expense.getDeductions() != null) {
			for (ExpenseCashAdvance deduction : expense.getDeductions()) {
				
				deduction.setExpenseId(expenseId);
				expenseCashAdvanceMapper.insert(deduction);		
				
				totalCashAdvance = totalCashAdvance.add(currencyRateService.exchange(deduction.getCurrency(), deduction.getAmount()));				
			}
		}
		expense.setExpenseCashAdvanceAmount(totalCashAdvance);
		
		expense.setStatus(ExpenseStatus.DRAFT.getValue());
			 		
		expense.setCreateTimestamp(new Timestamp(System.currentTimeMillis()));
		
		if(expense.getCustomerCostCentreMandatory() == null) {
			expense.setCustomerCostCentreMandatory("");
		}
		
		if(expense.getCustomerCostCentreName() == null) {
			expense.setCustomerCostCentreName("");
		}
		
		if(expense.getClarityProjectId() == null) {
			expense.setClarityProjectId("");
		}
		
		if(expense.getSiteId() == null) {
			expense.setSiteId(Site.Guangzhou.getId());
		}
		
		if(expense.getBillable() == null) {
			expense.setBillable(Billable.NO.getCode());
		}
				
		if(isNew) {
			expenseMapper.insert(expense);
		}else {
			expenseMapper.updateByPrimaryKeySelective(expense);
		}
		
		return Result.success(Operation.CREATE).id(expenseId).data(expense);
	}

	@Override
	public Result deleteExpense(String expenseId) {

		Expense expense = expenseMapper.selectByPrimaryKey(expenseId);
		
		if(expense == null) {
			throw new RecordNotFoundException(Expense.class,expenseId); 
		}
		
		if(!ExpenseStatus.DRAFT.getValue().equals(expense.getStatus())) {
			throw new OperationNotAllowException(Operation.DELETE,"Only allow to delete draft expense");
		}
				
		expenseMapper.deleteByPrimaryKey(expenseId);
		
		expenseDetailMapper.deleteByExample().where(ExpenseDetailDynamicSqlSupport.expenseId, isEqualTo(expenseId)).build().execute();
		
		expenseCashAdvanceMapper.deleteByExample().where(ExpenseCashAdvanceDynamicSqlSupport.expenseId, isEqualTo(expenseId)).build().execute();

		return Result.success(Operation.DELETE).id(expenseId);
	}
	
	@Override
	public Result submitExpense(String expenseId) {

		Expense expense = this.getExpense(expenseId, false).getData();
				
		if(!ExpenseStatus.DRAFT.getValue().equals(expense.getStatus())) {
			throw new OperationNotAllowException("Only allow to submit draft expense");
		}
		
//		expense.setStatus(ExpenseStatus.PENDING_APPROVAL.getValue());
//		
//		expense.setSubmittedDate(new Date(System.currentTimeMillis()));
//				
//		expenseMapper.updateByPrimaryKeySelective(expense);
		
		expenseMapper.updateExpenseStatus(expenseId, ExpenseStatus.PENDING_APPROVAL.getValue(), new Date(System.currentTimeMillis()));
		expenseMapper.updateExpenseDetailStatus(expenseId, ExpenseStatus.PENDING_APPROVAL.getValue());
		
		return Result.success(Operation.UPDATE).id(expenseId);
	}

	@Override
	public Result updateExpense(Expense expense) {
		
		expense.setCreateTimestamp(new Timestamp(System.currentTimeMillis()));
		
		expenseMapper.updateByPrimaryKeySelective(expense);

		return Result.success(Operation.UPDATE).id(expense.getExpenseId());
	}
	
	@Override
	public Result<List<ExpenseDetail>> getExpensePurposes(String expenseId){
		
		List<ExpenseDetail> purposes = expenseDetailMapper.selectByExample().where(ExpenseDetailDynamicSqlSupport.expenseId, isEqualTo(expenseId)).build().execute();
		
		return Result.success().data(purposes);
	}
	
	@Override
	public Result createExpensePurpose(ExpenseDetail purpose) {
		
		purpose.setCreateTimestamp(new Timestamp(System.currentTimeMillis()));
				
		String expenseDetailId = purpose.getExpenseDetailId();
		if(StringUtils.isEmpty(expenseDetailId)) {
			expenseDetailId = generateExpenseDetailId();
			purpose.setExpenseDetailId(expenseDetailId);
			purpose.setInvoiceId(expenseDetailId);
			if(purpose.getAmount() == null || StringUtils.isEmpty(purpose.getCurrency()) || purpose.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
				throw new ValidationFailureException("Amount or Currency is missing");
			}
			
			purpose.setStatus(ExpenseStatus.DRAFT.getValue());
			expenseDetailMapper.insert(purpose);
		}
		else {						
			expenseDetailMapper.updateByPrimaryKeySelective(purpose);			
		}
		
		expenseMapper.calcTotalAmount(purpose.getExpenseId());
		
		return Result.success(Operation.CREATE).data(purpose);
	}

	@Override
	public Result deleteExpensePurpose(String purposeId) {
		
		ExpenseDetail purpose = expenseDetailMapper.selectByPrimaryKey(purposeId);
		if(purpose != null) {
			expenseDetailMapper.deleteByPrimaryKey(purposeId);
						
			expenseMapper.calcTotalAmount(purpose.getExpenseId());
		}
				
		return Result.success(Operation.DELETE).id(purposeId);
	}

	@Override
	public Result<ExpenseDetail> getExpensePurpose(String expensePurposeId) {		
		return Result.success(Operation.SEARCH).data(expenseDetailMapper.selectByPrimaryKey(expensePurposeId));
	}

	private String generateExpenseId() {
		return "X"+RandomStringUtils.randomNumeric(9);
	}
	private String generateExpenseDetailId() {
		return "XD"+RandomStringUtils.randomAlphabetic(30);
	}
}
